﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Text;
using System.Threading.Tasks;
using System.Web.Hosting;

namespace ER.Dynamic
{
    public static class Emiter
    {
        public const string DynamicDllName = "ER.Dynamic.dll";
        private static Dictionary<string, Type> _runtimeTypes = new Dictionary<string, Type>();
        public static readonly AssemblyEmiter AssemblyEmiter;
        public static readonly ModuleEmiter ModuleEmiter;
        static Emiter()
        {
            AssemblyEmiter = new AssemblyEmiter(DynamicDllName);
            ModuleEmiter = AssemblyEmiter.CreateModuleEmiter("DynamicData");
        }
        public static TypeEmiter CreateTypeEmiter(String classFullName)
        {
            TypeEmiter typeEmiter = ModuleEmiter.CreateTypeEmiter(classFullName);
            return typeEmiter;
        }
        public static ModuleEmiter CreateModuleEmiter(this AssemblyEmiter assemblyEmiter, String moduleName)
        {
            return new ModuleEmiter(assemblyEmiter, moduleName);
        }
        public static Type GetType(string typeName)
        {
            return _runtimeTypes[typeName];
        }
        public static void RegisterType(Type type)
        {
            _runtimeTypes[type.FullName] = type;
        }
    }
    public class AssemblyEmiter
    {
        internal static readonly TypeConflictResolver TypeResovlver;
        public AssemblyName AssemblyName { get; private set; }
        public AssemblyBuilder AssemblyBuilder { get; private set; }
        static AssemblyEmiter()
        {
            TypeResovlver = new TypeConflictResolver();
            TypeResovlver.Bind(AppDomain.CurrentDomain);
        }
        public AssemblyEmiter(string assemblyName)
        {
            AssemblyName = new AssemblyName(assemblyName);
            AssemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(AssemblyName, AssemblyBuilderAccess.RunAndSave, HostingEnvironment.ApplicationPhysicalPath + "Bin");
        }
        public ModuleEmiter CreateModuleEmiter(string moduleName)
        {
            return new ModuleEmiter(this, moduleName);
        }
    }
    public class ModuleEmiter 
    {
        public AssemblyEmiter AssemblyEmiter { get; private set; }
        public ModuleBuilder ModuleBuilder { get; private set; }
        public string ModuleName { get; private set; }
        public ModuleEmiter(AssemblyEmiter assEmiter, String moduleName) 
        {
            AssemblyEmiter = assEmiter;
            ModuleName = moduleName;
            ModuleBuilder = assEmiter.AssemblyBuilder.DefineDynamicModule(moduleName);
        }
        public TypeEmiter CreateTypeEmiter(string typeName)
        {
            TypeEmiter type = new TypeEmiter(this, typeName);
            return type;
        }
        public TypeEmiter CreateTypeEmiter(string typeName, Type baseType)
        {
            TypeEmiter type = new TypeEmiter(this, typeName, baseType);
            return type;
        }
    }
    public class TypeEmiter
    {
        public ModuleEmiter ModuleEmiter { get; private set; }
        public Type Type{get; private set;}
        public TypeBuilder TypeBuilder { get; private set; }
        public string TypeName { get; private set; }
        public Type BaseType{get; set;}
        public Dictionary<string, PropertyEmiter> PropertyEmiters { get; set; }
        public List<ConstructorEmiter> ConstructorEmiters { get; set; }
        public TypeEmiter(ModuleEmiter moduleEmiter, string typeName) 
        {
            TypeName = typeName;
            BaseType = typeof(object);
            ModuleEmiter = moduleEmiter;
            TypeBuilder = moduleEmiter.ModuleBuilder.DefineType(typeName);
            PropertyEmiters = new Dictionary<string, PropertyEmiter>();
            ConstructorEmiters = new List<ConstructorEmiter>();
        }
        public TypeEmiter(ModuleEmiter moduleEmiter, string typeName, Type baseType)
            :this(moduleEmiter, typeName)
        {
            if (this.BaseType != null)
            {
                BaseType = baseType;
            }
        }
        public ConstructorEmiter AddConstructorEmiter(Type[] types)
        {
            return new ConstructorEmiter(this, types);
        }
        public PropertyEmiter AddPropertyEmiter(string propertyName, Type propertyType)
        {
            if (this.Type != null) 
            {
                throw new Exception("Type emited.  Cannot add property");
            }
            PropertyEmiter propEmiter = new PropertyEmiter(this, propertyName, propertyType);
            return propEmiter;
        }
        public object CreateObject(params object[] param) 
        {
            if (this.Type == null) 
            {
                Emit();
            }
            object result = Activator.CreateInstance(this.Type, param);
            return result;
        }
        public Type Emit()
        {
            if (this.Type == null) 
            {
                TypeBuilder.SetParent(BaseType);
                AssemblyEmiter.TypeResovlver.AddTypeBuilder(this.TypeBuilder);
                this.Type = this.TypeBuilder.CreateType();
                Emiter.RegisterType(this.Type);
            }
            System.Diagnostics.Debug.WriteLine(this.Type.AssemblyQualifiedName);
            return this.Type;
        }
        public static implicit operator Type(TypeEmiter typeEmiter)
        {
            return typeEmiter.TypeBuilder;
        }
        public object GetPropertyValue(object obj, string propertyName)
        {
            return this.Type.GetProperty(propertyName).GetValue(obj);
        }
        public void SetPropertyValue(object obj, string propertyName, object value)
        {
            this.Type.GetProperty(propertyName).SetValue(obj, value);
        }
    }
    
    public class PropertyEmiter
    {
        public TypeEmiter TypeEmiter { get; private set; }
        public string PropertyName { get; private set; }
        public Type PropertyType { get; private set; }
        public PropertyBuilder PropertyBuilder {get; private set;}
        public PropertyEmiter(TypeEmiter typeEmiter, string propertyName, Type propertyType)
        {
            TypeEmiter = typeEmiter;
            this.PropertyName = propertyName;
            this.PropertyType = propertyType;
            PropertyBuilder = createPropertyBuilder(typeEmiter.TypeBuilder, propertyName, propertyType);
            typeEmiter.PropertyEmiters.Add(PropertyName, this);
        }
        private PropertyBuilder createPropertyBuilder(TypeBuilder typeBuilder, string propertyName, Type propertyType)
        {
            // Add a private field of type int (Int32).
            FieldBuilder fbNumber = typeBuilder.DefineField(
                "m_" + propertyName,
                propertyType,
                FieldAttributes.Private);

            // Define a property named Number that gets and sets the private  
            // field. 
            // 
            // The last argument of DefineProperty is null, because the 
            // property has no parameters. (If you don't specify null, you must 
            // specify an array of Type objects. For a parameterless property, 
            // use the built-in array with no elements: Type.EmptyTypes)
            PropertyBuilder pbNumber = typeBuilder.DefineProperty(
                propertyName,
                PropertyAttributes.HasDefault,
                propertyType,
                null);

            // The property "set" and property "get" methods require a special
            // set of attributes.
            MethodAttributes getSetAttr = MethodAttributes.Public |
                MethodAttributes.SpecialName | MethodAttributes.HideBySig;

            // Define the "get" accessor method for Number. The method returns
            // an integer and has no arguments. (Note that null could be  
            // used instead of Types.EmptyTypes)
            MethodBuilder mbNumberGetAccessor = typeBuilder.DefineMethod(
                "get_" + propertyName,
                getSetAttr,
                propertyType,
                Type.EmptyTypes);
            ILGenerator numberGetIL = mbNumberGetAccessor.GetILGenerator();
            // For an instance property, argument zero is the instance. Load the  
            // instance, then load the private field and return, leaving the 
            // field value on the stack.
            numberGetIL.Emit(OpCodes.Ldarg_0);
            numberGetIL.Emit(OpCodes.Ldfld, fbNumber);
            numberGetIL.Emit(OpCodes.Ret);

            // Define the "set" accessor method for Number, which has no return 
            // type and takes one argument of type int (Int32).
            MethodBuilder mbNumberSetAccessor = typeBuilder.DefineMethod(
                "set_" + propertyName,
                getSetAttr,
                null,
                new Type[] { propertyType });

            ILGenerator numberSetIL = mbNumberSetAccessor.GetILGenerator();
            // Load the instance and then the numeric argument, then store the 
            // argument in the field.
            numberSetIL.Emit(OpCodes.Ldarg_0);
            numberSetIL.Emit(OpCodes.Ldarg_1);
            numberSetIL.Emit(OpCodes.Stfld, fbNumber);
            numberSetIL.Emit(OpCodes.Ret);

            // Last, map the "get" and "set" accessor methods to the 
            // PropertyBuilder. The property is now complete. 
            pbNumber.SetGetMethod(mbNumberGetAccessor);
            pbNumber.SetSetMethod(mbNumberSetAccessor);
            return pbNumber;
        }
        public void AddCustomAttribute(Type attributeType, string[] attributePropNames, object[] attributePropValues)
        {
            ConstructorInfo constructorInfo = attributeType.GetConstructor(new Type[]{});
            var attributePropInfo = attributePropNames.Select(at => attributeType.GetProperty(at)).ToArray();
            CustomAttributeBuilder customAttributeBuilder = new CustomAttributeBuilder(constructorInfo, new object[]{}, attributePropInfo, attributePropValues);
            this.PropertyBuilder.SetCustomAttribute(customAttributeBuilder);
        }
    }
    public class ConstructorEmiter
    {
        public TypeEmiter TypeEmiter { get; private set; }
        public Type[] Types { get; private set; }
        public ConstructorBuilder ConstructorBuilder { get; private set; }
        public ConstructorEmiter(TypeEmiter typeEmiter, Type[] types)
        {
            this.TypeEmiter = typeEmiter;
            Types = types;
            ConstructorBuilder constructorBuilder = this.TypeEmiter.TypeBuilder.DefineConstructor(MethodAttributes.Public, CallingConventions.Standard | CallingConventions.HasThis, types);
            this.ConstructorBuilder = constructorBuilder;
            if (this.TypeEmiter.BaseType != null)
            {
                ConstructorInfo baseConstructorInfo = this.TypeEmiter.BaseType.GetConstructor(types);
                var ilGenerator = constructorBuilder.GetILGenerator();
                ilGenerator.Emit(OpCodes.Ldarg_0);
                ilGenerator.Emit(OpCodes.Ldarg_1);
                ilGenerator.Emit(OpCodes.Call, baseConstructorInfo);
                ilGenerator.Emit(OpCodes.Ret);
            }
            typeEmiter.ConstructorEmiters.Add(this);
        }
    }
}
